package com.yb.boot.security.jwt.common.common;

import com.alibaba.fastjson.JSONObject;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
import java.util.Base64;
import java.util.Date;
import java.util.Map;

/**
 * Jwt生成和解析工具--支持传入秘钥和默认秘钥,传入过期时间和默认过期时间
 *====这个是融合两种生成两种生成方式的工具(推荐使用)
 * @author biaoyang
 * @date 2019/8/30 0030
 */
@Slf4j
public class JwtUtils {
    public static final String AUTHORIZATION_HEADER = "Bearer ";
    public static final String COMMA = ".";
    public static final Integer THREE = 3;
    public static final String ISS = "YangBiao";
    public static final String AUD = "BiaoYang";
    public static final long JWT_EXPIRED_SEVEN_DAYS = 7 * 60 * 60 * 1000;
    public static final long JWT_EXPIRED_THIRTY_MINUTES = 30 * 60 * 1000;
    public static final String BASE64_ENCODE_SECRET = "Snd0QmFzZTY0U2VjcmV0WWVz";

    /**
     * author biaoyang
     * Date: 2019/4/25 0025
     * Description:获取jwt唯一身份识别码jti
     *
     * @return
     */
    public static String createJti() {
        return String.valueOf(System.nanoTime());
    }

    /**
     * 通过加密算法和秘钥生成加密jwt的token的Key
     *
     * @param base64Secret 经过Base64编码的Secret(秘钥)
     * @return
     */
    private static Key getKey(String base64Secret) {
        byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(base64Secret);
        return new SecretKeySpec(apiKeySecretBytes, SignatureAlgorithm.HS512.getJcaName());
    }

    /**
     * 验证jwt的token的 签名
     *
     * @param jsonWebToken
     * @param key
     * @return
     */
    public static boolean verifySignature(String jsonWebToken, Key key) {
        return Jwts.parser()
                .setSigningKey(key)
                .isSigned(jsonWebToken);
    }

    /**
     * 生成accessToken
     * 使用了通过秘钥加密的key,再用一个加密算法加密key形成token的签名
     * 一般测试的时候用的是一个字符串作为秘钥加密生成签名,而没有用key
     * <p>(这个是直接放在只经过base64编码的荷载中,有必要的时候可以加密只有base64编码的token,对称或非对称加密)
     *
     * @param user         没有敏感信息的用户信息包装类对象
     * @param expiredTime  accessToken的过期时间(毫秒)
     * @param base64Secret 经过Base64编码的Secret(秘钥)
     * @return
     */
    public static String createAccessToken(LoginUser user, long expiredTime, String base64Secret) {
        //添加构成JWT的参数
        JwtBuilder builder = Jwts.builder().setHeaderParam("typ", "JWT")
                //设置授权者
                .setIssuer(ISS)
                //设置鉴权者
                .setAudience(AUD)
                //添加jwt的id,也就是jti
                .setId(user.getJti())
                //装填用户信息到荷载
                .addClaims((Map<String, Object>) JSONObject.toJSON(user))
                //设置subject
                .setSubject(user.getUsername())
                //设置签名信息
                .signWith(SignatureAlgorithm.HS512, getKey(base64Secret));
        //添加Token过期时间
        if (expiredTime > 0) {
            long expMillis = System.currentTimeMillis() + expiredTime;
            Date exp = new Date(expMillis);
            builder.setExpiration(exp).setNotBefore(new Date(System.currentTimeMillis()));
        }
        //生成JWT并为其加上前缀Bearer
        return AUTHORIZATION_HEADER + builder.compact();
    }

    /**
     * 使用默认过期时间和默认秘钥的方法--30分钟
     *
     * @param user 没有敏感信息的用户信息包装类对象
     * @return
     */
    public static String createAccessToken(LoginUser user) {
        //使用默认的过期时间,调用方法生成token
        return createAccessToken(user, JWT_EXPIRED_THIRTY_MINUTES, BASE64_ENCODE_SECRET);
    }

    /**
     * 使用默认过期时间的方法 --30分钟
     *
     * @param user   没有敏感信息的用户信息包装类对象
     * @param secret 经过Base64编码的Secret(秘钥)
     * @return
     */
    public static String createAccessToken(LoginUser user, String secret) {
        return createAccessToken(user, JWT_EXPIRED_THIRTY_MINUTES, secret);
    }

    /**
     * 使用默认秘钥的方法
     *
     * @param user        没有敏感信息的用户信息包装类对象
     * @param expiredTime accessToken的过期时间(毫秒)
     * @return
     */
    public static String createAccessToken(LoginUser user, long expiredTime) {
        return createAccessToken(user, expiredTime, BASE64_ENCODE_SECRET);
    }

    /**
     * 创建刷新token
     * 使用了通过秘钥加密的key,再用一个加密算法加密key形成token的签名
     *
     * @param username    用户名
     * @param expiredTime refreshToken的过期时间(毫秒)
     * @param secret      经过Base64编码的Secret(秘钥)
     * @return
     */
    public static String createRefreshToken(String username, long expiredTime, String secret) {
        //添加构成JWT的参数
        JwtBuilder builder = Jwts.builder().setHeaderParam("typ", "JWT")
                //设置授权者
                .setIssuer(ISS)
                //设置鉴权者
                .setAudience(AUD)
                //声明信息
                .claim("scope", "REFRESH")
                //设置主体名称
                .setSubject(username)
                //设置签名信息
                .signWith(SignatureAlgorithm.HS512, getKey(secret));
        //添加Token过期时间
        if (expiredTime > 0) {
            long expMillis = System.currentTimeMillis() + expiredTime;
            Date exp = new Date(expMillis);
            builder.setExpiration(exp).setNotBefore(new Date(System.currentTimeMillis()));
        }
        //生成JWT
        return builder.compact();
    }

    /**
     * 使用默认过期时间生成刷新token(时间是7天)
     *
     * @param username 用户名
     * @param secret   经过Base64编码的Secret(秘钥)
     * @return
     */
    public static String createRefreshToken(String username, String secret) {
        return createRefreshToken(username, JWT_EXPIRED_SEVEN_DAYS, secret);
    }

    /**
     * 使用默认的秘钥生成刷新token
     *
     * @param username    用户名
     * @param expiredTime refreshToken的过期时间(毫秒)
     * @return
     */
    public static String createRefreshToken(String username, long expiredTime) {
        return createRefreshToken(username, expiredTime, BASE64_ENCODE_SECRET);
    }

    /**
     * 使用默认过期时间和默认秘钥生成刷新token(时间是7天)
     *
     * @param username 用户名
     * @return
     */
    public static String createRefreshToken(String username) {
        return createRefreshToken(username, JWT_EXPIRED_SEVEN_DAYS, BASE64_ENCODE_SECRET);
    }

    /**
     * 校验token的合法性,并且获取荷载信息
     * 因为设定用的是LoginUser来封装数据到荷载,解析也是如此,如果解析荷载为空,就判定是token合法性认证失败
     * 设定荷载里必须要有一定的不敏感的用户信息,用来提供给业务用,或者前端展示用
     *
     * @param jwtToken jwt生成的token
     * @param secret   经过Base64编码的Secret(秘钥)
     * @return
     */
    public static LoginUser verifySignatureGetPayload(String jwtToken, String secret) {
        //判断token的合法性
        if (StringUtils.hasText(jwtToken) && jwtToken.startsWith(AUTHORIZATION_HEADER)) {
            //去掉头部的Bearer 前缀
            jwtToken = jwtToken.replaceFirst(AUTHORIZATION_HEADER, "");
            //验证jwtToken的签名信息
            if (verifySignature(jwtToken, getKey(secret))) {
                //对jwt的token进行切割判断-->注意坑:\\.点转义放在变量里,会导致转义失效,而导致字符串用点切割失败,所以这魔法就放着吧(可以替换成其他字符再处理)
                if (jwtToken.contains(COMMA) && jwtToken.split("\\.").length == THREE) {
                    //获取荷载内容,实测用DatatypeConverter.parseBase64Binary解析,会导致解析出的荷载是没有后面那个大括号的,会导致解析成LoginUser失败
                    String payload = new String(Base64.getUrlDecoder().decode(jwtToken.split("\\.")[1]));
                    //解析荷载(封装的时候也要是JSON转的对象,才能反过来解析出来)
                    if (StringUtils.hasText(payload)) {
                        try {
                            //解析用户信息,前提是封装信息的时候也是使用JSONObject字符串化的,否则会抛出异常
                            LoginUser loginUser = JSONObject.parseObject(payload, LoginUser.class);
                            //返回解析后的信息
                            return loginUser;
                        } catch (Exception e) {
                            log.info("jwtToken的载荷通过JSONObject.parseObject解析错误");
                        }
                    }
                }
            }
        }
        //不满足条件返回null
        return null;
    }

    /**
     * 使用默认秘钥验证jwtToken的合法性并返回载荷
     *
     * @param jwtToken jwt生成的token
     * @return
     */
    public static LoginUser verifySignatureGetPayload(String jwtToken) {
        return verifySignatureGetPayload(jwtToken, BASE64_ENCODE_SECRET);
    }

    /**
     * 获取当前访问的ip地址
     *
     * @param request
     * @return
     */
    public static String getIpAddress(HttpServletRequest request) {
        String ip = null;
        String unknown = "unknown";
        String[] headers = {"x-forwarded-for", "Proxy-Client-IP", "WL-Proxy-Client-IP",
                "HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR"};
        for (String header : headers) {
            ip = request.getHeader(header);
            if (StringUtils.hasText(ip) && !unknown.equalsIgnoreCase(ip)) {
                break;
            }
        }
        if (!StringUtils.hasText(ip) || unknown.equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
    }
}
